use bevy::{
    core_pipeline::{
        bloom::{Bloom, BloomCompositeMode},
        tonemapping::{DebandDither, Tonemapping},
    },
    color::palettes::{
        basic::WHITE,
        css::{GREEN, RED, ORANGE_RED},
    },
    math::{ops::powf, FloatPow},
    prelude::*,
    render::camera::{ScalingMode, SubCameraView, Viewport},
    platform::collections::{HashMap, HashSet},
    time::common_conditions::on_timer,
    reflect::TypePath,
    render::{
        mesh::MeshTag,
        render_resource::{AsBindGroup, ShaderRef},
    },
};
use std::time::Duration;
use rand::{seq::IteratorRandom, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

use crate::tutorials::bevy::{components::my_cpnt, events::my_ev};
use crate::tutorials::bevy::events::my_ev::*;
use crate::tutorials::bevy::res::{my_res::*, my_assets::*};
use super::game_sys::*;


/// Sets up an instanced grid of cubes, where each cube is colored based on an image that is
/// sampled in the vertex shader. The cubes are then animated in a spiral pattern.
///
/// This example demonstrates one use of automatic instancing and how to use `MeshTag` to use
/// external data in a custom material. For example, here we use the "index" of each cube to
/// determine the texel coordinate to sample from the image in the shader.
pub fn setup_instanced(
    mut commands: Commands,
    assets: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<CustomMaterialImage>>,
) {
    // We will use this image as our external data for our material to sample from in the vertex shader
    let image = assets.load("tex/eg.png");

    // Our single mesh handle that will be instanced
    let mesh_handle = meshes.add(Cuboid::from_size(Vec3::splat(0.01)));

    // Create the custom material with a reference to our texture
    // Automatic instancing works with any Material, including the `StandardMaterial`.
    // This custom material is used to demonstrate the optional `MeshTag` feature.
    let material_handle = materials.add(CustomMaterialImage {
        image: image.clone(),
    });

    // We're hardcoding the image dimensions for simplicity
    let image_dims = UVec2::new(256, 256);
    let total_pixels = image_dims.x * image_dims.y;

    for index in 0..total_pixels {
        // Get x,y from index - x goes left to right, y goes top to bottom
        let x = index % image_dims.x;
        let y = index / image_dims.x;

        // Convert to centered world coordinates
        let world_x = (x as f32 - image_dims.x as f32 / 2.0) / 50.0;
        let world_y = -((y as f32 - image_dims.y as f32 / 2.0) / 50.0); // Still need negative for world space

        commands.spawn((
            // For automatic instancing to take effect you need to
            // use the same mesh handle and material handle for each instance
            Mesh3d(mesh_handle.clone()),
            MeshMaterial3d(material_handle.clone()),
            // This is an optional component that can be used to help tie external data to a mesh instance
            MeshTag(index),
            Transform::from_xyz(world_x, world_y, 0.0),
        ));
    }

    // Camera
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 0.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

// 
pub fn setup_material_shader(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<CustomMaterialEmpty>>,
) {
    // cube
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::default())),
        MeshMaterial3d(materials.add(CustomMaterialEmpty {})),
        Transform::from_xyz(0.0, 0.5, 0.0),
    ));

    // camera
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

/// Set up a simple 3D scene
pub fn setup_subview(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let transform = Transform::from_xyz(-2.0, 3.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y);

    // Plane
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
        Transform::from_xyz(0.0, -0.5, 0.0),
    ));

    // Cube
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 0.5))),
        MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
        Transform::from_xyz(0.0, 0.5, 0.0),
    ));

    // Light
    commands.spawn((
        PointLight {
            shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 8.0, 4.0),
    ));

    // Main perspective camera:
    // The main perspective image to use as a comparison for the sub views.
    commands.spawn((
        Camera3d::default(),
        Camera::default(),
        my_cpnt::ExampleViewports::PerspectiveMain,
        transform,
    ));

    // Perspective camera right half:
    //
    // For this camera, the projection is perspective, and `size` is half the
    // width of the `full_size`, while the x value of `offset` is set to half
    // the value of the full width, causing the right half of the image to be
    // shown. Since the viewport has an aspect ratio of 1x1 and the sub view has
    // an aspect ratio of 1x2, the image appears stretched along the horizontal
    // axis.
    commands.spawn((
        Camera3d::default(),
        Camera {
            sub_camera_view: Some(SubCameraView {
                // The values of `full_size` and `size` do not have to be the
                // exact values of your physical viewport. The important part is
                // the ratio between them.
                full_size: UVec2::new(10, 10),
                // The `offset` is also relative to the values in `full_size`
                // and `size`
                offset: Vec2::new(5.0, 0.0),
                size: UVec2::new(5, 10),
            }),
            order: 1,
            ..default()
        },
        my_cpnt::ExampleViewports::PerspectiveStretched,
        transform,
    ));

    // Perspective camera moving:
    //
    // For this camera, the projection is perspective, and the offset is updated
    // continuously in 150 units per second in `move_camera_view`. Since the
    // `full_size` is 500x500, the image should appear to be moving across the
    // full image once every 3.3 seconds. `size` is a fifth of the size of
    // `full_size`, so the image will appear zoomed in.
    commands.spawn((
        Camera3d::default(),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(500, 500),
                offset: Vec2::ZERO,
                size: UVec2::new(100, 100),
            }),
            order: 2,
            ..default()
        },
        transform,
        my_cpnt::ExampleViewports::PerspectiveMoving,
        my_cpnt::MovingCameraMarker,
    ));

    // Perspective camera different aspect ratio:
    //
    // For this camera, the projection is perspective, and the aspect ratio of
    // the sub view (2x1) is different to the aspect ratio of the full view
    // (2x2). The aspect ratio of the sub view matches the aspect ratio of
    // the viewport and should show an unstretched image of the top half of the
    // full perspective image.
    commands.spawn((
        Camera3d::default(),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(800, 800),
                offset: Vec2::ZERO,
                size: UVec2::new(800, 400),
            }),
            order: 3,
            ..default()
        },
        my_cpnt::ExampleViewports::PerspectiveControl,
        transform,
    ));

    // Main orthographic camera:
    //
    // The main orthographic image to use as a comparison for the sub views.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            order: 4,
            ..default()
        },
        my_cpnt::ExampleViewports::OrthographicMain,
        transform,
    ));

    // Orthographic camera left half:
    //
    // For this camera, the projection is orthographic, and `size` is half the
    // width of the `full_size`, causing the left half of the image to be shown.
    // Since the viewport has an aspect ratio of 1x1 and the sub view has an
    // aspect ratio of 1x2, the image appears stretched along the horizontal axis.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(2, 2),
                offset: Vec2::ZERO,
                size: UVec2::new(1, 2),
            }),
            order: 5,
            ..default()
        },
        my_cpnt::ExampleViewports::OrthographicStretched,
        transform,
    ));

    // Orthographic camera moving:
    //
    // For this camera, the projection is orthographic, and the offset is
    // updated continuously in 150 units per second in `move_camera_view`. Since
    // the `full_size` is 500x500, the image should appear to be moving across
    // the full image once every 3.3 seconds. `size` is a fifth of the size of
    // `full_size`, so the image will appear zoomed in.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(500, 500),
                offset: Vec2::ZERO,
                size: UVec2::new(100, 100),
            }),
            order: 6,
            ..default()
        },
        transform,
        my_cpnt::ExampleViewports::OrthographicMoving,
        my_cpnt::MovingCameraMarker,
    ));

    // Orthographic camera different aspect ratio:
    //
    // For this camera, the projection is orthographic, and the aspect ratio of
    // the sub view (2x1) is different to the aspect ratio of the full view
    // (2x2). The aspect ratio of the sub view matches the aspect ratio of
    // the viewport and should show an unstretched image of the top half of the
    // full orthographic image.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(200, 200),
                offset: Vec2::ZERO,
                size: UVec2::new(200, 100),
            }),
            order: 7,
            ..default()
        },
        my_cpnt::ExampleViewports::OrthographicControl,
        transform,
    ));
}

// 
pub fn generate_bodies(
    time: Res<Time<Fixed>>,
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let mesh = meshes.add(Sphere::new(1.0).mesh().ico(3).unwrap());

    let color_range = 0.5..1.0;
    let vel_range = -0.5..0.5;

    // We're seeding the PRNG here to make this example deterministic for testing purposes.
    // This isn't strictly required in practical use unless you need your app to be deterministic.
    let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);
    for _ in 0..my_cpnt::NUM_BODIES {
        let radius: f32 = rng.random_range(0.1..0.7);
        let mass_value = FloatPow::cubed(radius) * 10.;

        let position = Vec3::new(
            rng.random_range(-1.0..1.0),
            rng.random_range(-1.0..1.0),
            rng.random_range(-1.0..1.0),
        )
        .normalize()
            * ops::cbrt(rng.random_range(0.2f32..1.0))
            * 15.;

        commands.spawn((
            my_cpnt::BodyBundle {
                mesh: Mesh3d(mesh.clone()),
                material: MeshMaterial3d(materials.add(Color::srgb(
                    rng.random_range(color_range.clone()),
                    rng.random_range(color_range.clone()),
                    rng.random_range(color_range.clone()),
                ))),
                mass: my_cpnt::Mass(mass_value),
                acceleration: my_cpnt::Acceleration(Vec3::ZERO),
                last_pos: my_cpnt::LastPos(
                    position
                        - Vec3::new(
                            rng.random_range(vel_range.clone()),
                            rng.random_range(vel_range.clone()),
                            rng.random_range(vel_range.clone()),
                        ) * time.timestep().as_secs_f32(),
                ),
            },
            Transform {
                translation: position,
                scale: Vec3::splat(radius),
                ..default()
            },
        ));
    }

    // add bigger "star" body in the center
    let star_radius = 1.;
    commands
        .spawn((
            my_cpnt::BodyBundle {
                mesh: Mesh3d(meshes.add(Sphere::new(1.0).mesh().ico(5).unwrap())),
                material: MeshMaterial3d(materials.add(StandardMaterial {
                    base_color: ORANGE_RED.into(),
                    emissive: LinearRgba::from(ORANGE_RED) * 2.,
                    ..default()
                })),

                mass: my_cpnt::Mass(500.0),
                ..default()
            },
            Transform::from_scale(Vec3::splat(star_radius)),
            my_cpnt::Star,
        ))
        .with_child(PointLight {
            color: Color::WHITE,
            range: 100.0,
            radius: star_radius,
            ..default()
        });
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 10.5, -30.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

// 
pub fn add_people(mut commands: Commands) {
    commands.spawn((my_cpnt::Person, my_cpnt::Name("Elaina Proctor".to_string())));
    commands.spawn((my_cpnt::Person, my_cpnt::Name("Renzo Hume".to_string())));
    commands.spawn((my_cpnt::Person, my_cpnt::Name("Zayna Nieves".to_string())));
}
// 
const X_EXTENT: f32 = 900.;

pub fn setup_wireframe(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    let shapes = [
        meshes.add(Circle::new(50.0)),
        meshes.add(CircularSector::new(50.0, 1.0)),
        meshes.add(CircularSegment::new(50.0, 1.25)),
        meshes.add(Ellipse::new(25.0, 50.0)),
        meshes.add(Annulus::new(25.0, 50.0)),
        meshes.add(Capsule2d::new(25.0, 50.0)),
        meshes.add(Rhombus::new(75.0, 100.0)),
        meshes.add(Rectangle::new(50.0, 100.0)),
        meshes.add(RegularPolygon::new(50.0, 6)),
        meshes.add(Triangle2d::new(
            Vec2::Y * 50.0,
            Vec2::new(-50.0, -50.0),
            Vec2::new(50.0, -50.0),
        )),
    ];
    let num_shapes = shapes.len();

    for (i, shape) in shapes.into_iter().enumerate() {
        // Distribute colors evenly across the rainbow.
        let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);

        commands.spawn((
            Mesh2d(shape),
            MeshMaterial2d(materials.add(color)),
            Transform::from_xyz(
                // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
                -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
                0.0,
                0.0,
            ),
        ));
    }

    #[cfg(not(target_arch = "wasm32"))]
    commands.spawn((
        Text::new("Press space to toggle wireframes"),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(12.0),
            left: Val::Px(12.0),
            ..default()
        },
    ));
}

// 
pub fn setup_2d_viewport(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    window: Single<&Window>,
) {
    let window_size = window.resolution.physical_size().as_vec2();

    // Initialize centered, non-window-filling viewport
    commands.spawn((
        Camera2d,
        Camera {
            viewport: Some(Viewport {
                physical_position: (window_size * 0.125).as_uvec2(),
                physical_size: (window_size * 0.75).as_uvec2(),
                ..default()
            }),
            ..default()
        },
    ));

    // Create a minimal UI explaining how to interact with the example
    commands.spawn((
        Text::new(
            "Move the mouse to see the circle follow your cursor.\n\
                    Use the arrow keys to move the camera.\n\
                    Use the comma and period keys to zoom in and out.\n\
                    Use the WASD keys to move the viewport.\n\
                    Use the IJKL keys to resize the viewport.",
        ),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(12.0),
            left: Val::Px(12.0),
            ..default()
        },
    ));

    // Add mesh to make camera movement visible
    commands.spawn((
        Mesh2d(meshes.add(Rectangle::new(40.0, 20.0))),
        MeshMaterial2d(materials.add(Color::from(GREEN))),
    ));

    // Add background to visualize viewport bounds
    commands.spawn((
        Mesh2d(meshes.add(Rectangle::new(50000.0, 50000.0))),
        MeshMaterial2d(materials.add(Color::linear_rgb(0.01, 0.01, 0.01))),
        Transform::from_translation(Vec3::new(0.0, 0.0, -200.0)),
    ));
}

// 
pub fn setup_bloom(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
    asset_server: Res<AssetServer>,
) {
    commands.spawn((
        Camera2d,
        Camera {
            hdr: true, // 1. HDR is required for bloom
            clear_color: ClearColorConfig::Custom(Color::BLACK),
            ..default()
        },
        Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended
        Bloom::default(),           // 3. Enable bloom for the camera
        DebandDither::Enabled,      // Optional: bloom causes gradients which cause banding
    ));

    // Sprite
    commands.spawn(Sprite {
        image: asset_server.load("tex\\hdr.png"),
        color: Color::srgb(5.0, 5.0, 5.0),
        custom_size: Some(Vec2::splat(160.0)),
        ..default()
    });

    // Circle mesh
    commands.spawn((
        Mesh2d(meshes.add(Circle::new(100.))),
        MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
        Transform::from_translation(Vec3::new(-200., 0., 0.)),
    ));

    // Hexagon mesh
    commands.spawn((
        Mesh2d(meshes.add(RegularPolygon::new(100., 6))),
        MeshMaterial2d(materials.add(Color::srgb(6.25, 9.4, 9.1))),
        Transform::from_translation(Vec3::new(200., 0., 0.)),
    ));

    // UI
    commands.spawn((
        Text::default(),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(12.0),
            left: Val::Px(12.0),
            ..default()
        },
    ));
}

// 
pub fn setup_scene_for_disable(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    let named_shapes = [
        (
            Name::new("Annulus"), 
            meshes.add(Annulus::new(25.0, 50.0)),
        ),
        (
            Name::new("Bestagon"),
            meshes.add(RegularPolygon::new(50.0, 6)),
        ),
        (
            Name::new("Rhombus"), 
            meshes.add(Rhombus::new(75.0, 100.0)),
        ),
    ];
    let num_shapes = named_shapes.len();

    for (i, (name, shape)) in named_shapes.into_iter().enumerate() {
        // Distribute colors evenly across the rainbow.
        let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);

        commands.spawn((
            name,
            my_cpnt::DisableOnClick,
            Mesh2d(shape),
            MeshMaterial2d(materials.add(color)),
            Transform::from_xyz(
                // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
                -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
                0.0,
                0.0,
            ),
        ));
    }
}

pub fn display_instructions(mut commands: Commands) {
    commands.spawn((
        Text::new(
            "Click an entity to disable it.\n\nPress Space to re-enable all disabled entities.",
        ),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(12.0),
            left: Val::Px(12.0),
            ..default()
        },
    ));
}

// 
pub fn setup_mine(mut commands: Commands) {
    commands.spawn(Camera2d);
    commands.spawn((
        Text::new(
            "Click on a \"Mine\" to trigger it.\n\
            When it explodes it will trigger all overlapping mines.",
        ),
        Node {
            position_type: PositionType::Absolute,
            top: Val::Px(12.),
            left: Val::Px(12.),
            ..default()
        },
    ));

    let mut rng = ChaCha8Rng::seed_from_u64(19878367467713);

    commands
        .spawn(my_cpnt::Mine::random(&mut rng))
        // Observers can watch for events targeting a specific entity.
        // This will create a new observer that runs whenever the Explode event
        // is triggered for this spawned entity.
        .observe(explode_mine);

    // We want to spawn a bunch of mines. We could just call the code above for each of them.
    // That would create a new observer instance for every Mine entity. Having duplicate observers
    // generally isn't worth worrying about as the overhead is low. But if you want to be maximally efficient,
    // you can reuse observers across entities.
    //
    // First, observers are actually just entities with the Observer component! The `observe()` functions
    // you've seen so far in this example are just shorthand for manually spawning an observer.
    let mut observer = Observer::new(explode_mine);

    // As we spawn entities, we can make this observer watch each of them:
    for _ in 0..1000 {
        let entity = commands.spawn(my_cpnt::Mine::random(&mut rng)).id();
        observer.watch_entity(entity);
    }
    // By spawning the Observer component, it becomes active!
    commands.spawn(observer);
}

// event bubbling. Our events will target the armor, and if the armor isn't strong enough to block
// the attack it will continue up and hit the goblin.
pub fn setup_ob_propagation(mut commands: Commands) {
    commands
        .spawn((Name::new("Goblin"), my_cpnt::HitPoints(50)))
        .observe(take_damage)
        .with_children(|parent| {
            parent
                .spawn((Name::new("Helmet"), my_cpnt::Armor(5)))
                .observe(block_attack);
            parent
                .spawn((Name::new("Socks"), my_cpnt::Armor(10)))
                .observe(block_attack);
            parent
                .spawn((Name::new("Shirt"), my_cpnt::Armor(15)))
                .observe(block_attack);
        });
}